In this notebook we investigate the transitions that a crystal of ultra-cold ions in a Penning trap undergoes as the rotation frequency of the ions in the trap is increased. As the rotation frequency increases, the radial confinement becomes stronger. Eventually it becomes energetically favourable, to stack several crystal of ions on top of one another. The frequencies at which these transitions occur are well understood theoretically and serve as a verification of the numerical integration of the ions motion.


In [1]:
%matplotlib notebook
import matplotlib.pyplot as plt
import matplotlib.lines
import numpy as np
import mode_analysis_code
import coldatoms
import ion_trapping

First we need to find an initial configuration of the ions close to their ground state


In [2]:
mode_analysis = mode_analysis_code.ModeAnalysis(N=127,
                                                Vtrap=(0.0, -1750.0, -1970.0),
                                                Vwall=1.0,
                                                frot=185.0)

In [3]:
mode_analysis.run()

In [4]:
mode_analysis.show_axial_Evals();



In [5]:
def create_ensemble(uE, omega_z, mass, charge):
    num_ions = int(uE.size / 2)
    x = uE[:num_ions]
    y = uE[num_ions:]
    r = np.sqrt(x**2 + y**2)
    r_hat = np.transpose(np.array([x / r, y / r]))
    phi_hat = np.transpose(np.array([-y / r, x / r]))
    v = np.zeros([num_ions, 2], dtype=np.float64)
    for i in range(num_ions):
        v[i, 0] = omega_z * r[i] * phi_hat[i, 0]
        v[i, 1] = omega_z * r[i] * phi_hat[i, 1]
    
    ensemble = coldatoms.Ensemble(num_ions)
    for i in range(num_ions):
        ensemble.x[i, 0] = x[i]
        ensemble.x[i, 1] = y[i]
        ensemble.x[i, 2] = 0.0
        ensemble.v[i, 0] = v[i, 0]
        ensemble.v[i, 1] = v[i, 1]
        ensemble.v[i, 2] = 0.0
    
    ensemble.ensemble_properties['mass'] = mass
    ensemble.ensemble_properties['charge'] = charge
    
    return ensemble

Forces


In [6]:
coulomb_force = coldatoms.CoulombForce()

class TrapPotential(object):

    def __init__(self, kz, delta, omega, phi_0):
        self.kz = kz
        self.kx = -(0.5 + delta) * kz
        self.ky = -(0.5 - delta) * kz
        self.phi_0 = phi_0
        self.phi = phi_0
        self.omega = omega
        self.trap_potential = coldatoms.HarmonicTrapPotential(self.kx, self.ky, self.kz)

    def reset_phase(self):
        self.phi = self.phi_0
            
    def force(self, dt, ensemble, f):
        self.phi += self.omega * 0.5 * dt
        self.trap_potential.phi = self.phi
        self.trap_potential.force(dt, ensemble, f)
        self.phi += self.omega * 0.5 * dt

trap_potential = TrapPotential(2.0 * mode_analysis.Coeff[2], mode_analysis.Cw, mode_analysis.wrot, np.pi / 2.0)

After we change the rotation frequency we need the ions to settle into a new steady state. This requires a damping of the azimuthal motion of the ions. In the rotating frame, the ions should settle to a state where they co-rotate with the trap at the new frequency.


In [7]:
class AngularDamping(object):

    def __init__(self, omega, kappa_theta):
        self.omega = omega
        self.kappa_theta = kappa_theta
            
    def dampen(self, dt, ensemble):
        ion_trapping.angular_damping(self.omega, self.kappa_theta * dt, ensemble.x, ensemble.v)

In [8]:
class AxialDamping(object):
    """Doppler cooling along z without recoil."""

    def __init__(self, kappa):
        """kappa is the damping rate."""
        self.kappa = kappa
            
    def dampen(self, dt, ensemble):
        ion_trapping.axial_damping(self.kappa * dt, ensemble.v)

In [9]:
def evolve_ensemble_with_damping(dt, t_max, ensemble, Bz, forces, dampings):
    num_steps = int(np.floor(t_max / dt))
    for i in range(num_steps):
        coldatoms.bend_kick(dt, Bz, ensemble, forces, num_steps=1)
        for d in dampings:
            d.dampen(dt, ensemble)
    fractional_dt = t_max - (num_steps * dt)
    if (fractional_dt / dt > 1.0e-6):
        coldatoms.bend_kick(fractional_dt, Bz, ensemble, forces, num_steps=1)
        for d in dampings:
            d.dampen(fractional_dt, ensemble)

In [10]:
def radius(x):
    return np.sqrt(x[:, 0]**2 + x[:, 1]**2)

def speed(v):
    return np.sqrt(v[:, 0]**2 + v[:, 1]**2 + v[:, 2]**2)

def radial_velocity(x, v):
    num_ptcls = v.shape[0]
    velocity = np.zeros(num_ptcls)
    for i in range(num_ptcls):
        r_hat = np.copy(x[i, :2])
        r_hat /= np.linalg.norm(r_hat)
        velocity[i] = r_hat.dot(v[i, :2])
    return velocity

def angular_velocity(x, v):
    num_ptcls = v.shape[0]
    velocity = np.zeros(num_ptcls)
    for i in range(num_ptcls):
        r_hat = np.copy(x[i, :2])
        r_hat /= np.linalg.norm(r_hat)
        v_r = r_hat * (r_hat.dot(v[i, :2]))
        velocity[i] = np.linalg.norm(v[i, :2] - v_r)
    return velocity

In [11]:
def evolve_to_steady_state(dt=1.0e-9, t_max=1.0e-8, dampings=[],
                           omega=2.0 * np.pi * 180.0e3, vz_perturbation=1.0e-3):
    my_ensemble = create_ensemble(mode_analysis.uE,
                                  omega,
                                  mode_analysis.m_Be,
                                  mode_analysis.q)
    my_ensemble.v[:, 2] += np.random.normal(loc=0.0, scale=vz_perturbation, size=my_ensemble.num_ptcls)

    x_0 = np.copy(my_ensemble.x)
    v_0 = np.copy(my_ensemble.v)
    
    trap_potential.phi = np.pi / 2.0
    trap_potential.omega = omega

    evolve_ensemble_with_damping(
        dt,
        t_max,
        my_ensemble,
        mode_analysis.B,
        [coulomb_force, trap_potential],
        dampings)
    
    return (my_ensemble, x_0, v_0)

In [12]:
omega = 1.2 * mode_analysis.wrot
t_max = 2.0e1 * 2.0 * np.pi / omega
dt=1.0e-9
print(t_max)
kappa = 1.0e7
my_ensemble, x0, v0 = evolve_to_steady_state(t_max=t_max, dt=dt, omega=omega,
                                             dampings=[AngularDamping(omega, kappa)]);


9.00900900900901e-05
---------------------------------------------------------------------------
KeyboardInterrupt                         Traceback (most recent call last)
<ipython-input-12-b990327a6587> in <module>()
      5 kappa = 1.0e7
      6 my_ensemble, x0, v0 = evolve_to_steady_state(t_max=t_max, dt=dt, omega=omega,
----> 7                                              dampings=[AngularDamping(omega, kappa)]);

<ipython-input-11-4660f59d232e> in evolve_to_steady_state(dt, t_max, dampings, omega, vz_perturbation)
     19         mode_analysis.B,
     20         [coulomb_force, trap_potential],
---> 21         dampings)
     22 
     23     return (my_ensemble, x_0, v_0)

<ipython-input-9-d63a24b3844a> in evolve_ensemble_with_damping(dt, t_max, ensemble, Bz, forces, dampings)
      2     num_steps = int(np.floor(t_max / dt))
      3     for i in range(num_steps):
----> 4         coldatoms.bend_kick(dt, Bz, ensemble, forces, num_steps=1)
      5         for d in dampings:
      6             d.dampen(dt, ensemble)

/Users/dmeiser/python3.6-sandbox/lib/python3.6/site-packages/coldatoms/bend_kick.py in bend_kick(dt, Bz, ensemble, forces, num_steps, reference_impl)
     50         updater(0.5 * dt, omegaB, ensemble.x, ensemble.v)
     51 
---> 52         f = np.zeros_like(ensemble.v)
     53 
     54         for i in range(num_steps - 1):

/Users/dmeiser/python3.6-sandbox/lib/python3.6/site-packages/numpy/core/numeric.py in zeros_like(a, dtype, order, subok)
    144     # needed instead of a 0 to get same result as zeros for for string dtypes
    145     z = zeros(1, dtype=res.dtype)
--> 146     multiarray.copyto(res, z, casting='unsafe')
    147     return res
    148 

KeyboardInterrupt: 

In [14]:
plt.figure(figsize=(3,3))
plt.plot(radius(my_ensemble.x), angular_velocity(my_ensemble.x, my_ensemble.v), 'o')
plt.plot(radius(x0), omega * radius(x0), '.')


Out[14]:
[<matplotlib.lines.Line2D at 0x104de1978>]

In [15]:
plt.figure(figsize=(3,3))
plt.plot(my_ensemble.x[:,0], my_ensemble.x[:,2], 'o')


Out[15]:
[<matplotlib.lines.Line2D at 0x104e758d0>]

In [16]:
np.std(my_ensemble.x[:,2])


Out[16]:
4.2312516732229102e-06

In [17]:
w = mode_analysis.wrot
omegas = np.linspace(0.9 * w, 3.0 * w, 100)
thicknesses = []
for omega in omegas:
    t_max = 16.0e0 * 2.0 * np.pi / omega
    dt=1.0e-9
    kappa = 5.0e6
    my_ensemble, x0, v0 = evolve_to_steady_state(t_max=t_max, dt=dt, omega=omega,
                                             dampings=[AngularDamping(omega, kappa), AxialDamping(0.1 * kappa)])
    thickness = np.std(my_ensemble.x[:,2])
    print(omega, thickness)
    thicknesses.append(thickness)
thicknesses = np.array(thicknesses)
print(omegas)
print(thicknesses)


1046150.35365 2.95687222445e-21
1070807.09599 5.70295852013e-21
1095463.83833 9.2616602248e-21
1120120.58067 1.81317436151e-20
1144777.32301 2.75528290684e-20
1169434.06535 5.17005775807e-20
1194090.8077 7.88237042644e-20
1218747.55004 1.20849503314e-06
1243404.29238 1.94224742617e-06
1268061.03472 2.48587582179e-06
1292717.77706 2.93539056608e-06
1317374.51941 3.31713106219e-06
1342031.26175 3.65531782428e-06
1366688.00409 3.95851095976e-06
1391344.74643 4.18664988712e-06
1416001.48877 4.41496474541e-06
1440658.23111 4.64804980802e-06
1465314.97346 4.87883058812e-06
1489971.7158 5.11893800404e-06
1514628.45814 5.33421022896e-06
1539285.20048 5.53905814574e-06
1563941.94282 5.72103249714e-06
1588598.68517 5.93135202303e-06
1613255.42751 6.12861567348e-06
1637912.16985 6.27090149693e-06
1662568.91219 6.43685193386e-06
1687225.65453 6.58863513077e-06
1711882.39687 6.73067672601e-06
1736539.13922 6.89270812851e-06
1761195.88156 7.03753743956e-06
1785852.6239 7.1804913487e-06
1810509.36624 7.3234925767e-06
1835166.10858 7.48060949216e-06
1859822.85093 7.59573227844e-06
1884479.59327 7.71593755141e-06
1909136.33561 7.83752471584e-06
1933793.07795 7.94634657083e-06
1958449.82029 8.06693280238e-06
1983106.56263 8.1711862863e-06
2007763.30498 8.30069564788e-06
2032420.04732 8.38938896801e-06
2057076.78966 8.48889842941e-06
2081733.532 8.57821093642e-06
2106390.27434 8.66620464782e-06
2131047.01669 8.76315045986e-06
2155703.75903 8.86219236953e-06
2180360.50137 8.96103451855e-06
2205017.24371 9.04511803093e-06
2229673.98605 9.1407753819e-06
2254330.72839 9.22700034429e-06
2278987.47074 9.31747421163e-06
2303644.21308 9.3972899051e-06
2328300.95542 9.48316686498e-06
2352957.69776 9.57752734376e-06
2377614.4401 9.6571962136e-06
2402271.18244 9.73643460728e-06
2426927.92479 9.8083029796e-06
2451584.66713 9.86863969235e-06
2476241.40947 9.94735825262e-06
2500898.15181 1.00211473525e-05
2525554.89415 1.00832141551e-05
2550211.6365 1.01600651603e-05
2574868.37884 1.02325578716e-05
2599525.12118 1.02884721008e-05
2624181.86352 1.03587984922e-05
2648838.60586 1.04304149588e-05
2673495.3482 1.04760756071e-05
2698152.09055 1.0542241035e-05
2722808.83289 1.06056644436e-05
2747465.57523 1.06684179836e-05
2772122.31757 1.07341463987e-05
2796779.05991 1.07913292167e-05
2821435.80226 1.08397513392e-05
2846092.5446 1.08933123766e-05
2870749.28694 1.09540283696e-05
2895406.02928 1.10021491696e-05
2920062.77162 1.10704121007e-05
2944719.51396 1.11222881325e-05
2969376.25631 1.11802396522e-05
2994032.99865 1.1219401939e-05
3018689.74099 1.12790935945e-05
3043346.48333 1.13233559706e-05
3068003.22567 1.13766556736e-05
3092659.96802 1.14317863677e-05
3117316.71036 1.14728990878e-05
3141973.4527 1.15231457144e-05
3166630.19504 1.15680186639e-05
3191286.93738 1.16110141575e-05
3215943.67972 1.16634895987e-05
3240600.42207 1.17096449183e-05
3265257.16441 1.17487368821e-05
3289913.90675 1.17969282199e-05
3314570.64909 1.18413844287e-05
3339227.39143 1.18885586409e-05
3363884.13378 1.19325269201e-05
3388540.87612 1.19685470733e-05
3413197.61846 1.20175366264e-05
3437854.3608 1.20512393522e-05
3462511.10314 1.2094787256e-05
3487167.84548 1.21327131742e-05
[ 1046150.3536454   1070807.09598721  1095463.83832902  1120120.58067083
  1144777.32301264  1169434.06535446  1194090.80769627  1218747.55003808
  1243404.29237989  1268061.0347217   1292717.77706351  1317374.51940532
  1342031.26174713  1366688.00408894  1391344.74643075  1416001.48877256
  1440658.23111437  1465314.97345618  1489971.715798    1514628.45813981
  1539285.20048162  1563941.94282343  1588598.68516524  1613255.42750705
  1637912.16984886  1662568.91219067  1687225.65453248  1711882.39687429
  1736539.1392161   1761195.88155791  1785852.62389972  1810509.36624154
  1835166.10858335  1859822.85092516  1884479.59326697  1909136.33560878
  1933793.07795059  1958449.8202924   1983106.56263421  2007763.30497602
  2032420.04731783  2057076.78965964  2081733.53200145  2106390.27434327
  2131047.01668508  2155703.75902689  2180360.5013687   2205017.24371051
  2229673.98605232  2254330.72839413  2278987.47073594  2303644.21307775
  2328300.95541956  2352957.69776137  2377614.44010318  2402271.18244499
  2426927.92478681  2451584.66712862  2476241.40947043  2500898.15181224
  2525554.89415405  2550211.63649586  2574868.37883767  2599525.12117948
  2624181.86352129  2648838.6058631   2673495.34820491  2698152.09054672
  2722808.83288853  2747465.57523035  2772122.31757216  2796779.05991397
  2821435.80225578  2846092.54459759  2870749.2869394   2895406.02928121
  2920062.77162302  2944719.51396483  2969376.25630664  2994032.99864845
  3018689.74099026  3043346.48333208  3068003.22567389  3092659.9680157
  3117316.71035751  3141973.45269932  3166630.19504113  3191286.93738294
  3215943.67972475  3240600.42206656  3265257.16440837  3289913.90675018
  3314570.64909199  3339227.3914338   3363884.13377562  3388540.87611743
  3413197.61845924  3437854.36080105  3462511.10314286  3487167.84548467]
[  2.95687222e-21   5.70295852e-21   9.26166022e-21   1.81317436e-20
   2.75528291e-20   5.17005776e-20   7.88237043e-20   1.20849503e-06
   1.94224743e-06   2.48587582e-06   2.93539057e-06   3.31713106e-06
   3.65531782e-06   3.95851096e-06   4.18664989e-06   4.41496475e-06
   4.64804981e-06   4.87883059e-06   5.11893800e-06   5.33421023e-06
   5.53905815e-06   5.72103250e-06   5.93135202e-06   6.12861567e-06
   6.27090150e-06   6.43685193e-06   6.58863513e-06   6.73067673e-06
   6.89270813e-06   7.03753744e-06   7.18049135e-06   7.32349258e-06
   7.48060949e-06   7.59573228e-06   7.71593755e-06   7.83752472e-06
   7.94634657e-06   8.06693280e-06   8.17118629e-06   8.30069565e-06
   8.38938897e-06   8.48889843e-06   8.57821094e-06   8.66620465e-06
   8.76315046e-06   8.86219237e-06   8.96103452e-06   9.04511803e-06
   9.14077538e-06   9.22700034e-06   9.31747421e-06   9.39728991e-06
   9.48316686e-06   9.57752734e-06   9.65719621e-06   9.73643461e-06
   9.80830298e-06   9.86863969e-06   9.94735825e-06   1.00211474e-05
   1.00832142e-05   1.01600652e-05   1.02325579e-05   1.02884721e-05
   1.03587985e-05   1.04304150e-05   1.04760756e-05   1.05422410e-05
   1.06056644e-05   1.06684180e-05   1.07341464e-05   1.07913292e-05
   1.08397513e-05   1.08933124e-05   1.09540284e-05   1.10021492e-05
   1.10704121e-05   1.11222881e-05   1.11802397e-05   1.12194019e-05
   1.12790936e-05   1.13233560e-05   1.13766557e-05   1.14317864e-05
   1.14728991e-05   1.15231457e-05   1.15680187e-05   1.16110142e-05
   1.16634896e-05   1.17096449e-05   1.17487369e-05   1.17969282e-05
   1.18413844e-05   1.18885586e-05   1.19325269e-05   1.19685471e-05
   1.20175366e-05   1.20512394e-05   1.20947873e-05   1.21327132e-05]

In [18]:
omegas = np.linspace(0.9 * w, 3.0 * w, 100)

In [19]:
plt.figure()
plt.plot(omegas/(2.0*np.pi*1.0e3), thicknesses*1.0e3)
plt.xlabel(r'$\omega_z/(2\pi kHz)$')
plt.ylabel(r'$\Delta z/{\rm mm}$')


Out[19]:
<matplotlib.text.Text at 0x10701e898>

In [20]:
plt.figure()
plt.plot(omegas/(2.0*np.pi*1.0e3), thicknesses*1.0e3)
plt.xlim([180, 220])
plt.ylim([-1.0e-4,5.0e-3])
plt.xlabel(r'$\omega_z/(2\pi kHz)$')
plt.ylabel(r'$\Delta z/{\rm mm}$')


Out[20]:
<matplotlib.text.Text at 0x10df114e0>

Frequency sweeps

Now we want to simulate sweeps of the rotation frequency. We assume that the frequency varies piecewise linearly, i.e. with a constant sweep rate $d \omega / dt$ on each sub-interval.


In [95]:
def make_sweep(omega_0, t_hold, t_sweep, delta_omega, t_hold_2):
    """Creates an omega(t).
    
    The frequency is held at omega_0 for t_hold. Then it increases linearly
    with a sweet rate delta_omega/t_sweep for t_sweep. For t_hold + t_sweep < t < t_hold + t_sweep +t_hold_2
    the frequency is constant again."""
    
    ts = np.array([0,
                   t_hold,
                   t_hold + t_sweep,
                   t_hold + t_sweep + t_hold_2,
                   t_hold + t_sweep + t_hold_2 + t_sweep
                  ])
    omegas = np.array([omega_0, omega_0, omega_0 + delta_omega, omega_0 + delta_omega, omega_0])
    omega_of_t = lambda t: np.interp(t, ts, omegas, left=omega_0, right=omegas[-1])
    return omega_of_t

In [101]:
t_max = 4.0e-3
# We start out at 180kHz
omega_0 = 2.0 * np.pi * 180.0e3
# And stay there for a couple of revolutions
t_hold = 1.0e-5
# Then we sweep by 20kHz in 20 revolutions
t_sweep = 1.0e-3
delta_omega = 2.0 * np.pi * 25e3
# Stay at that level for a millisecond, then sweep back
t_hold_2 = 1.0e-3
my_omega_of_t = make_sweep(omega_0, t_hold, t_sweep, delta_omega, t_hold_2)
t = np.linspace(-1.0e-5, t_max, 200)
omega = my_omega_of_t(t)
plt.figure()
plt.plot(t / 1.0e-6, omega / (1.0e3 * 2.0 * np.pi));
plt.xlabel(r'$t/\mu s$')
plt.ylabel(r'$\omega / (2\pi kHz)$');



In [102]:
t_max = 4.0e-3
dt = 1.0e-9

# Definition of the frequency sweep
# We start out at 180kHz
omega_0 = 2.0 * np.pi * 180.0e3
# And stay there for a couple of revolutions
t_hold = 1.0e-5
# Then we sweep by 20kHz in 20 revolutions
t_sweep = 1.0e-3
delta_omega = 2.0 * np.pi * 25e3
# Stay at that level for a millisecond, then sweep back
t_hold_2 = 1.0e-3
my_omega_of_t = make_sweep(omega_0, t_hold, t_sweep, delta_omega, t_hold_2)


kappa_theta = 5.0e6
kappa_z = 1.0e6


my_trap_potential = TrapPotential(2.0 * mode_analysis.Coeff[2],
                                  mode_analysis.Cw,
                                  my_omega_of_t(0.0),
                                  np.pi / 2.0)
my_axial_damping = AxialDamping(kappa=kappa_z)
my_angular_damping = AngularDamping(omega=my_omega_of_t(0.0), kappa_theta=kappa_theta)


# We start out with the steady state distribution with some Gaussian noise in
# the z velocities to seed the multi-plane instability
vz_perturbation = 1.0e-3
my_ensemble = create_ensemble(mode_analysis.uE,
                              my_omega_of_t(0.0),
                              mode_analysis.m_Be,
                              mode_analysis.q)
my_ensemble.v[:, 2] += np.random.normal(loc=0.0, scale=vz_perturbation, size=my_ensemble.num_ptcls)

snap_shot_times = np.linspace(0, t_max, 4000)
snap_shot_times = np.round(snap_shot_times / dt) * dt

snapshots = []
thicknesses = []
t = -dt
while t < t_max:
    # Get the rotation frequency at the mid-point of the time integration interval
    omega = my_omega_of_t(t + 0.5*dt)
    
    # And use it for the rotating wall potential and the angular damping
    my_trap_potential.omega = omega
    my_angular_damping.omega = omega
    
    # Now take a time step
    evolve_ensemble_with_damping(
        dt,
        dt,
        my_ensemble,
        mode_analysis.B,
        [coulomb_force, trap_potential],
        [my_axial_damping, my_angular_damping])
    t += dt
    
    # Record snapshot
    if np.any(np.abs(snap_shot_times - t) < 0.5 * dt):
        snapshots.append((t, omega, my_ensemble.copy()))
    
    # Record thickness of cloud
    thicknesses.append(np.std(my_ensemble.x[:,2]))

In [109]:
s = snapshots[590]
print('t == ', s[0])
print('omega == ', my_omega_of_t(s[0]) / (2.0 * np.pi))
plt.figure()
plt.plot(s[2].x[:, 0], s[2].x[:, 2], 'o')


t ==  0.0005901480000042465
omega ==  194503.70000010618
Out[109]:
[<matplotlib.lines.Line2D at 0x1616680f0>]

In [117]:
s = snapshots[1000]
print('t == ', s[0])
print('omega / 2pi == ', my_omega_of_t(s[0]) / (2.0 * np.pi))
plt.figure()
plt.plot(s[2].x[:, 0], s[2].x[:, 2], 'o')


t ==  0.0010002500000081355
omega / 2pi ==  204756.2500002034
Out[117]:
[<matplotlib.lines.Line2D at 0x156c5e0b8>]

In [113]:
ts=np.linspace(0, t_max, len(thicknesses))

In [115]:
plt.figure()
plt.plot(my_omega_of_t(ts)/(2.0*np.pi),thicknesses,'.')


Out[115]:
[<matplotlib.lines.Line2D at 0x17893d7f0>]

In [ ]: